/**
 * File: TraverseHelper.java
 * Created by: mhaimel
 * Created on: Jun 7, 2010
 * CVS:  $Id: TraverseHelper.java 1.0 Jun 7, 2010 5:03:10 PM mhaimel Exp $
 */
package uk.ac.ebi.curtain.model.graph.curtain;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import uk.ac.ebi.curtain.model.graph.Arc;
import uk.ac.ebi.curtain.model.graph.Node;
import uk.ac.ebi.curtain.model.graph.NodeEdge;
import uk.ac.ebi.curtain.utils.BitUtil;

/**
 * @author mhaimel
 *
 */
public class TraverseHelper {
	public static final int USED_FLAG = (int)Math.pow(2, 0);
	public static final int DEFAULT_LIB_FLAG = (int)Math.pow(2, 1);
	public static final int DEFAULT_LIB_SUPPORT_FLAG = (int)Math.pow(2, 2);
	public static final int REPEAT_FLAG = (int)Math.pow(2, 3);
	public static final int REPEAT_LOW_FLAG = (int)Math.pow(2, 4);
	public static final int REPEAT_FREE_FLAG = (int)Math.pow(2, 5);

	private final Set<Node<ContigInfo, ReadWrapper>> nodes = new HashSet<Node<ContigInfo,ReadWrapper>>();
	private final Set<Node<ContigInfo, ReadWrapper>> allNodes = new HashSet<Node<ContigInfo,ReadWrapper>>();
	private final Queue<Node<ContigInfo, ReadWrapper>> jumpTrace = new LinkedBlockingDeque<Node<ContigInfo,ReadWrapper>>();
	private final ConcurrentSkipListMap<Double, Queue<Node<ContigInfo, ReadWrapper>>> sortedQueue = 
					new ConcurrentSkipListMap<Double, Queue<Node<ContigInfo,ReadWrapper>>>();
	private final AtomicInteger categories;
	private final AtomicInteger currLib = new AtomicInteger(0);
	private final AtomicLong seqCount = new AtomicLong(0);
	private final AtomicLong readBpCount = new AtomicLong(0);
	private final long minSize;
	private final long maxSize;
	private final AtomicBoolean repeatAware = new AtomicBoolean(false);

	
	public TraverseHelper(Integer categories, long minSize, long maxSize) {
		this.categories = new AtomicInteger(categories);
		this.minSize = minSize;
		this.maxSize = maxSize;
	}
	
	public void setRepeatAware(boolean repeatAware) {
		this.repeatAware.set(repeatAware);
	}
	public boolean getRepeatAware() {
		return repeatAware.get();
	}

	public void registerSeed(Node<ContigInfo, ReadWrapper> seed) {
		setFlag(seed, DEFAULT_LIB_FLAG);
		_add2Queue(seed,0d);
	}
	
	private boolean _add2Queue(Node<ContigInfo, ReadWrapper> node, Double weight) {
		if(!_isRegistered(node)){
			Queue<Node<ContigInfo, ReadWrapper>> queue = this.sortedQueue.get(weight);
			if(null == queue){
				queue = new LinkedBlockingQueue<Node<ContigInfo,ReadWrapper>>();
				Queue<Node<ContigInfo, ReadWrapper>> prevQueue = this.sortedQueue.putIfAbsent(weight, queue);
				if(null != prevQueue){
					queue = prevQueue;
				}
			}
			queue.add(node);
			return true;
		} else {
			return false;
		}
	}

	private boolean _isRegistered(Node<ContigInfo, ReadWrapper> node) {
		return this.nodes.contains(node);
	}
	
	public boolean hasEmptyQueue(){
		return queueSize() == 0;
	}
	
	public boolean hasMoreCategories(){
		return (currLib.get()+1) < categories.get();
	}
	
	public boolean hasReachedSequenceLimit(){
		return this.seqCount.get() >= this.maxSize;
	}
	
	public void resetCount(){
		resetSequenceCount(0);
		setReadBpCount(0);
	}
	
	public void resetSequenceCount(long count){
		this.seqCount.set(count);
	}
	
	public long getReadBpCount() {
		return readBpCount.get();
	}
	
	public void setReadBpCount(long readBpCount) {
		this.readBpCount.set(readBpCount);
	}

	/**
	 * Reset Library used to 0
	 */
	public void resetLibrary(){
		resetLibrary(0);
	}
	
	public void resetLibrary(int library) {
		this.currLib.set(library);		
	}
	
	public int getCurrentLibrary(){
		return this.currLib.get();
	}

	/**
	 * Switches to the next library 
	 * 
	 */
	public void nextLibrary(){
		this.currLib.incrementAndGet();
	}
	
	public long getSequenceLength(){
		return this.seqCount.get();
	}

	private void _registerNode(Node<ContigInfo, ReadWrapper> node){
		if(!hasFlag(node, USED_FLAG)){
//		if(nodes.add(node)){
			nodes.add(node);
			this.seqCount.addAndGet(node.getValue().getLength());
			for(int i = 0; i < node.getValue().getCategories(); ++i){
				this.readBpCount.addAndGet(node.getValue().getCoverageBpCount(i));
			}
			this.jumpTrace.add(node);
			setFlag(node,USED_FLAG);
		} 
	}

	public void fillQueue(int limit) {
		fillQueue(limit, false);
	}
	
	public void fillQueue(int limit, boolean force) {
		int cat = getCurrentLibrary();
		for(Node<ContigInfo, ReadWrapper> node : this.jumpTrace){
			if(queueSize() >= limit){
				break;
			}
			if((hasFlag(node, DEFAULT_LIB_FLAG) || hasFlag(node, DEFAULT_LIB_SUPPORT_FLAG)) || force){
				Map<Node<ContigInfo, ReadWrapper>, Double> found = _findNodes(cat, node);
				for(Entry<Node<ContigInfo, ReadWrapper>, Double> otherNode : found.entrySet()){
					_add2Queue(otherNode.getKey(),otherNode.getValue());
				}
			}
		}
	}
	
	private Integer queueSize(){
		int cnt = 0;
		for(Entry<Double, Queue<Node<ContigInfo, ReadWrapper>>> otherNode : sortedQueue.entrySet()){
			cnt+= otherNode.getValue().size();
		}
		return cnt;
	}

	public void processQueue() {
		int cat = getCurrentLibrary();
		Node<ContigInfo, ReadWrapper> node = null;
		while((node = _nextValidNode()) != null){
			ContigInfo ctgInfo = node.getValue();
//			if(ctgInfo.getLength()>= getMinSize()){
				Map<Node<ContigInfo, ReadWrapper>, Double> found = _findNodes(cat, node);
				boolean hasDefaultFlag = hasFlag(node, DEFAULT_LIB_FLAG);
				if(hasDefaultFlag || !found.isEmpty()){
					if(!hasDefaultFlag){
						setFlag(node, DEFAULT_LIB_SUPPORT_FLAG);
					}
					// search node was found by default library -> normal behaviour
					for(Entry<Node<ContigInfo, ReadWrapper>, Double> otherNode : found.entrySet()){
						setFlag(otherNode.getKey(), DEFAULT_LIB_FLAG);
						_add2Queue(otherNode.getKey(),otherNode.getValue());
					}
				}				
//			}
			_registerNode(node);
			if(hasReachedSequenceLimit()){
				break;
			}			
		}
	}

	private long getMinSize() {
		return minSize;
	}

	private Map<Node<ContigInfo, ReadWrapper>, Double> _findNodes(int cat, Node<ContigInfo, ReadWrapper> node) {
		Map<Node<ContigInfo, ReadWrapper>,Double> found = new HashMap<Node<ContigInfo,ReadWrapper>, Double>();
		if(getRepeatAware()){
			if(hasFlag(node, REPEAT_FLAG)){ 
				/* Repeat flag (ignore any other flag)
				 * -> STOP search, because found down to Repeat connection
				 */
				return found;
			} else if(hasFlag(node, REPEAT_LOW_FLAG)
//					&& !hasFlag(node, REPEAT_FREE_FLAG) TODO check if this makes sense to remove it
					){
				/* weak repeat signal and NO other NON repetitive connection found
				 * -> STOP search
				 */
				return found;
			} else {
				/* a) weak repeat signal and Repeat FREE connection found
				 * b) Repeat FREE connection ONLY
				 * -> continue
				 */
			}
		}		
		ContigInfo currInfo = node.getValue();
		for(NodeEdge ne : NodeEdge.values()){
			if(!currInfo.hasPartialFlag(cat, ne)){
				for(Arc<ContigInfo,ReadWrapper> arc : node.getArcs(ne)){
					CategoryReadInfo info = arc.getValue().getInfo(cat);
					if(null != info){
						Node<ContigInfo, ReadWrapper> otherNode = arc.getOtherNode(node);
						if(getRepeatAware()){
							Integer mult = info.getMultiplicity();
							int rep = info.getRepeatMarkers();
							double pcRep = ((double) rep) / mult.doubleValue();
							if(pcRep > 0.5){
								setFlag(otherNode, REPEAT_FLAG);
							} else if (pcRep == 0){
								setFlag(otherNode, REPEAT_FREE_FLAG);
							} else {
								setFlag(otherNode, REPEAT_LOW_FLAG);
							}
						}
						if(_isValidNode(otherNode) && !_isRegistered(otherNode)){
							Double weightSum = info.getWeightSum();
							Integer multiplicity = info.getMultiplicity();
							if(multiplicity > 0){
								weightSum = weightSum / multiplicity;
							} else {
								weightSum = 0d;
							}
							found.put(otherNode,weightSum);
						}
					}
				}
			}
		}
		return found;
	}

	private Node<ContigInfo, ReadWrapper> _nextValidNode() {
		while(!hasEmptyQueue()){
			while(!sortedQueue.isEmpty()){
				Double lastKey = sortedQueue.lastKey();
				Queue<Node<ContigInfo, ReadWrapper>> queue = sortedQueue.get(lastKey);
				if(queue.isEmpty()){
					sortedQueue.remove(lastKey);
				} else {
					if(queue.size() == 1){
						sortedQueue.remove(lastKey);
					}
					Node<ContigInfo, ReadWrapper> node = queue.remove();
					if(_isValidNode(node)){						
						return node;
					}
				}
			}
		}
		return null;
	}

	private boolean _isValidNode(Node<ContigInfo, ReadWrapper> node) {
		return node.isActive() && !hasFlag(node, USED_FLAG);
	}

	private static boolean hasFlag(Node<ContigInfo, ReadWrapper> node, int flag) {
		return BitUtil.hasFlag(node.getStatus().get(), flag);
	}

	private static void setFlag(Node<ContigInfo, ReadWrapper> node, int flag) {
		while(true){
			int val = node.getStatus().get();
			int updated = BitUtil.setFlag(val,flag);
			if(node.getStatus().compareAndSet(val, updated)){
				break;
			}
		}
	}

	private void removeFlag(Node<ContigInfo, ReadWrapper> node, int flag) {
		while(true){
			int val = node.getStatus().get();
			int updated = BitUtil.removeFlag(val,flag);
			if(node.getStatus().compareAndSet(val, updated)){
				break;
			}
		}
	}

	public Set<Node<ContigInfo, ReadWrapper>> getNodes() {
		return new HashSet<Node<ContigInfo,ReadWrapper>>(this.nodes);
	}
	
	public static boolean gotUsed(Node<ContigInfo, ReadWrapper> node){
		return hasFlag(node, USED_FLAG) || hasFlag(node, DEFAULT_LIB_FLAG) || hasFlag(node, DEFAULT_LIB_SUPPORT_FLAG);
	}

	public void finish() {
		for(Node<ContigInfo,ReadWrapper> node : this.nodes){
			if(node.getValue().getLength() < this.getMinSize()){
				removeFlag(node,USED_FLAG);
			}
		}
		for(Node<ContigInfo,ReadWrapper> node : this.allNodes){
			if(node.getValue().getLength() < this.getMinSize()){
				removeFlag(node,USED_FLAG);
				removeFlag(node,REPEAT_FREE_FLAG);
			}
		}
		for(Entry<Double, Queue<Node<ContigInfo, ReadWrapper>>> entry : this.sortedQueue.entrySet()){
			for(Node<ContigInfo,ReadWrapper> node : entry.getValue()){
				removeFlag(node,DEFAULT_LIB_FLAG);
				removeFlag(node,DEFAULT_LIB_SUPPORT_FLAG);
				removeFlag(node,REPEAT_FREE_FLAG);
			}
		}
	}

	public void newCluster() {
		this.allNodes.addAll(this.nodes);
		this.nodes.clear();
		this.resetCount();
	}

	
}
